#include <stdio.h>
#include <stdlib.h>

void func3(char **pString);

/**
 * 内存的深入讲解。
 *
 * 内存四区模型：代码段、数据段、堆、栈。除此之外还有内核空间。
 * 堆空间的特点：需要手动申请，需要手动释放
 * 栈空间的特点: 自动分配，自动回收
 *
 */
int main() {
    // 1、在实际工作中，我们需要存储多个数据时往往可以想到使用数组，但是数组的长度在定义后是固定的，往往不够灵活。
    // 2、我们可以根据需要存储的数据，先定义一个指针变量，然后再根据实际需要通过malloc函数分配内存空间。
    /*
        void *__cdecl malloc(size_t _Size);
        该库函数来源于头文件<stdlib.h>，所以需要先引入后才可以进行使用。
        功能:向系统申请size个字节的内存空间，并且返回一个指针，该指针指向被分配的内存空间的首地址，
            且申请的内存空间是在"堆"上的。堆上的空间是需要手动申请、手动释放的!!否则会造成"内存泄漏"。
     */
    int *point;
    //假如我们需要存储4个int类型的数据。
    // TIPS:malloc申请分配的空间是以字节作为单位的。
    point = (int *) malloc(4 * sizeof(int)); //向系统申请(在堆上分配) 4*sizeof(int)字节的内存空间
    printf("指针指向申请到的堆上的内存空间的首地址:%p\n", point);
    printf("存储指针变量的空间的首地址:%p\n", &point);
    //通过指针变量来操作申请到的堆空间
    point[0] = 10;
    point[1] = 20;
    *(point + 2) = 30;
    *(point + 3) = 40;
    printf("指针指向空间的首地址存储的值:%d\n", *point);

    /*
         void __cdecl free(void *_Memory); 来源于头文件<stdlib.h>
         功能: 释放指针所指向的内存空间
         TIPS: free函数并不会修改指针变量的值!但是free执行完以后指针所指向的原来那块地址空间中的内容时不确定的!!
         那么释放空间到底做了什么事呢？告诉操作系统这块内存空间可以被别人使用了!!
     */
    free(point);
    printf("释放后指针指向的内存空间的首地址:%p\n", point);
    printf("释放后指针指向空间的首地址存储的值:%d\n", *point);
    //一般的我们在释放一个指针以后，需要将该指针变量的值赋值为NULL，这样做的目的是为了防止指针变为野指针!
    //这样做是为了防止这个野指针将别的指针指向空间的内容修改掉(因为操作系统可能会把free的空间分配给别的指针)
    //且因为赋值了NULL，所以调试时就可能产生段错误，这种错误相对来说是更容易找到的。
    point = NULL;

    /**
     * #include <stdlib.h>
     * void *__cdecl realloc(void *_Memory,size_t _NewSize);
     * 功能:在堆上分配一块newSize字节单位的新内存空间，并且将原本指针所指向的空间中的内容拷贝到新的内存空间中，最后返回新内存地址的首地址。
     * TIPS:新分配的空间的首地址有可能与之前分配的空间首地址一样，也可能不一样(由操作系统来判断)。
     * TIPS:realloc会隐式地帮你释放掉指针原来所指向的内存空间(除非新分配的空间用回本来的空间)
     *
     */
    point = (int *) realloc(point, 5 * sizeof(int));
    printf("指针指向新的申请到的堆上的内存空间的首地址:%p\n", point);
    printf("指针指向新的空间的首地址存储的值:%d\n", *point);


    char *tp;
    //假设我们希望指针指向某个函数调用结束后"在函数体中申请的堆空间的首地址"——用意是不需要自己来判断究竟需要分配多少空间而交由函数来解决
    //实际业务举例:领月饼——你去领月饼，原本需要自己带个容器去装，但是因为你不知道你会领到几个，所以你需要自己判断究竟需要拿多大的容器过去，
    //           但是如果将分配的工作交付给函数，则相当于容器由发月饼的人自行提供，他自然会给你安排好刚好装下的容器的，你只要负责领就行了。
    //既然我们希望让tp指向一块堆空间，其实就是希望对tp进行赋值，赋值为在函数中申请的堆空间的首地址。
    // 如何在函数中对tp进行赋值呢？必须在调用函数的时候传递tp的地址（也就是"传址"）
    printf("当前定义指针指向的首地址:%p\n", tp);
    printf("存储当前定义指针变量的首地址:%p\n", &tp);
    func3(&tp);
    printf("调用函数过后，在函数中为指针分配了空间并赋予了初始值:%s\n", tp);
    //记得用完之后释放(拿完月饼后将容器还给发月饼的人)
    free(tp);

    return 0;
}

/**
 * 二级指针作为函数的形参
 * @param pString 一个二级指针形参，类型char **，存储的是实参指针的地址。pString=&实参指针。*pString=*(&实参指针)=p。
 */
void func3(char **pString) {
    printf("此函数中二级指针存储的正是实参指针的地址:%p\n", pString);
    printf("通过传址在当前形参指针访问实参指针所指向的首地址:%p\n", *pString);
    *pString = (char *) malloc(10);//在堆上申请了10个字节
    **pString = 'H';
    (*pString)[1] = 'E';
    (*pString)[2] = 'L';
    *(*pString + 3) = 'L';
    *(*pString + 4) = 'O';
}